package cn.javacc.tools;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;

/**
 * 
 * @author Chi
 */
public class TemplateBuilder {
    static ScriptEngine engine;

    static {
        ScriptEngineManager engineManager = new ScriptEngineManager();
        engine = engineManager.getEngineByName("js");
    }

    private String template;
    private List<TemplateFragment> stack;
    Bindings bindings;

    public TemplateBuilder(String template) {
        this.template = template;
        this.stack = new ArrayList<TemplateFragment>();

        int depth = 0;
        for (int i = 0; i < this.template.length() - 1; i++) {
            char c = this.template.charAt(i);
            if (c == '<' && i + 1 < this.template.length() && this.template.charAt(i + 1) == '%') {
                int end = this.template.indexOf("%>", i + 1);
                if (end > i) {
                    int start = i + 2;

                    for (int j = start; j < end; j++) {
                        char ch = this.template.charAt(j);
                        if (Character.isWhitespace(ch)) {
                            start++;
                        } else if (ch == '=') {
                            TemplateFragment f = new TemplateFragment(this.template, i, end + 2, j + 1, end, depth, stack.size());
                            stack.add(f);
                            break;
                        } else if (ch == '(') {
                            if (this.template.substring(start, j).equals("if")) {
                                int end0 = this.template.lastIndexOf(')', end - 1);
                                TemplateFragment f = new IfTemplateFragment(this.template, i, end + 2, j + 1, end0, depth, stack.size());
                                stack.add(f);
                                depth++;
                                break;
                            } else if (this.template.substring(start, j).equals("for")) {
                                int end0 = this.template.lastIndexOf(')', end - 1);
                                TemplateFragment f = new ForTemplateFragment(this.template, i, end + 2, j + 1, end0, depth, stack.size());
                                stack.add(f);
                                depth++;
                                break;
                            } else {
                                int end0 = this.template.lastIndexOf(')', end - 1);
                                TemplateFragment f = new FunctionTemplateFragment(this.template, i, end + 2, j + 1, end0, depth, stack.size());
                                stack.add(f);
                                break;
                            }
                        } else if (ch == '}') {
                            depth--;
                            TemplateFragment f = new EndTemplateFragment(this.template, i, end + 2, 0, 0, depth, stack.size());
                            stack.add(f);
                            break;
                        } else if (ch == '{') {
                            if (this.template.substring(start, j).equals("else")) {
                                TemplateFragment f = new ElseTemplateFragment(this.template, i, end + 2, 0, 0, depth, stack.size());
                                stack.add(f);
                                depth++;
                                break;
                            }
                        } else {
                            // DO NOTHING
                        }
                    }
                    i = end + 1;
                }
            }
        }
    }

    public TemplateBuilder set(Map<String, Object> context) {
        this.bindings = new SimpleBindings(context);
        return this;
    }

    public String build() {
        StringBuilder b = new StringBuilder();
        int start = 0;
        for (int i = 0; i < this.stack.size(); i++) {
            TemplateFragment f = this.stack.get(i);
            b.append(this.template, start, f.start);
            i = f.eval(bindings, b, stack);
            start = this.stack.get(i).end;
        }
        if (start < this.template.length()) {
            b.append(this.template, start, this.template.length());
        }
        return b.toString();
    }

    class TemplateFragment {
        String template;
        String expression;
        int start;
        int end;
        int expressionStart;
        int expressionEnd;
        int depth;
        int pointer;
        int type = 0;

        int eval(Bindings context, StringBuilder b, List<TemplateFragment> stack) {
            try {
                Object value = engine.eval(expression, context);
                b.append(String.valueOf(value));
            } catch (ScriptException e) {
                e.printStackTrace();
            }
            return pointer;
        }

        public TemplateFragment(String template, int start, int end, int expStart, int expEnd, int depth, int p) {
            this.template = template;
            this.start = start;
            this.end = end;
            this.expressionStart = expStart;
            this.expressionEnd = expEnd;
            this.depth = depth;
            this.pointer = p;
            expression = this.template.substring(expressionStart, expressionEnd);
        }

        public String toString() {
            return this.template.substring(start, end) + ": " + this.depth;
        }
    }

    class EndTemplateFragment extends TemplateFragment {
        public EndTemplateFragment(String template, int start, int end, int expStart, int expEnd, int depth, int p) {
            super(template, start, end, expStart, expEnd, depth, p);
            type = -1;
        }

        int eval(Bindings context, StringBuilder b, List<TemplateFragment> stack) {
            return this.pointer;
        }
    }

    class ForTemplateFragment extends TemplateFragment {
        String variable;

        public ForTemplateFragment(String template, int start, int end, int expStart, int expEnd, int depth, int p) {
            super(template, start, end, expStart, expEnd, depth, p);
            type = 3;
            String[] parts = this.expression.split(":");
            variable = parts[0].trim();
            this.expression = parts[1].trim();
        }

        @SuppressWarnings("unchecked")
        int eval(Bindings context, StringBuilder b, List<TemplateFragment> stack) {
            try {
                Object value = engine.eval(expression, context);
                int nextPointer = this.pointer;

                while (++nextPointer < stack.size() && stack.get(nextPointer).depth > this.depth)
                    ;

                if (value != null) {
                    if (value.getClass().isArray()) {
                        this.renderList(context, b, stack, Arrays.asList((Object[]) value), this.pointer + 1, nextPointer);
                    } else if (List.class.isAssignableFrom(value.getClass())) {
                        this.renderList(context, b, stack, (List<Object>) value, this.pointer + 1, nextPointer);
                    }
                }

                if (stack.get(nextPointer).type == -1) {
                    return nextPointer;
                } else {
                    return nextPointer--;
                }
            } catch (ScriptException e) {
                e.printStackTrace();
                throw new RuntimeException("Invalid For expression.");
            }
        }

        void renderList(Bindings context, StringBuilder b, List<TemplateFragment> stack, List<Object> list, int start0, int end) {
            int i = 0;
            for (Iterator<?> iterator = list.iterator(); iterator.hasNext(); i++) {
                context.put(variable, iterator.next());
                context.put("__index", i);
                int start = this.end;
                for (int j = start0; j < end; j++) {
                    b.append(this.template, start, stack.get(j).start);
                    j = stack.get(j).eval(context, b, stack);
                    start = stack.get(j).end;
                }

                b.append(this.template.substring(stack.get(end - 1).end, stack.get(end).start));
            }
        }

    }

    class IfTemplateFragment extends TemplateFragment {
        public IfTemplateFragment(String template, int start, int end, int expStart, int expEnd, int depth, int p) {
            super(template, start, end, expStart, expEnd, depth, p);
            type = 1;
        }

        int eval(Bindings context, StringBuilder b, List<TemplateFragment> stack) {
            String expression = this.template.substring(expressionStart, expressionEnd);
            try {
                Object value = engine.eval(expression, context);
                if (value instanceof Boolean) {
                    if ((Boolean) value) {
                        int start = this.end;
                        int i = this.pointer + 1;
                        for (; i < stack.size(); i++) {
                            TemplateFragment f = stack.get(i);
                            if (f.depth > this.depth) {
                                b.append(this.template, start, f.start);
                                i = f.eval(context, b, stack);
                                start = f.end;
                            } else if (f.depth == this.depth) {
                                if (f.type == -1) {
                                    b.append(this.template, stack.get(i - 1).end, f.start);
                                    if (i + 1 < stack.size()) {
                                        if (stack.get(i + 1).type == 2) {
                                            TemplateFragment elseF = stack.get(i + 1);
                                            b.append(this.template, f.end, elseF.start);
                                            i = i + 1;
                                            while (i < stack.size() && stack.get(++i).depth > elseF.depth)
                                                ;
                                            if (stack.get(i).type == -1) {
                                                return i;
                                            }
                                            return i--;
                                        }
                                    }
                                    return i;
                                } else {
                                    return i--;
                                }
                            }
                        }
                        return i--;
                    } else {
                        int i = this.pointer;
                        while (stack.get(++i).depth > this.depth && i < stack.size())
                            ;
                        if (stack.get(i).type == -1) {
                            if (i + 1 < stack.size()) {
                                TemplateFragment f = stack.get(i + 1);
                                if (f.type == 2) {
                                    int start = f.end;
                                    int j = i + 2;
                                    for (; j < stack.size(); j++) {
                                        if (stack.get(j).depth > f.depth) {
                                            b.append(this.template, start, stack.get(j).start);
                                            j = stack.get(j).eval(context, b, stack);
                                            start = stack.get(j).end;
                                        } else if (stack.get(j).depth == f.depth) {
                                            if (stack.get(j).type == -1) {
                                                b.append(this.template, start, stack.get(j).start);
                                                return j;
                                            } else {
                                                return j--;
                                            }
                                        }
                                    }
                                }
                            }
                            return i;
                        }
                        return i--;
                    }
                } else {
                    throw new RuntimeException("Invalid If expression.");
                }
            } catch (ScriptException e) {
                e.printStackTrace();
                throw new RuntimeException("Invalid If expression.");
            }
        }
    }

    class ElseTemplateFragment extends TemplateFragment {
        public ElseTemplateFragment(String template, int start, int end, int expStart, int expEnd, int depth, int p) {
            super(template, start, end, expStart, expEnd, depth, p);
            type = 2;
        }

        int eval(Bindings context, StringBuilder b, List<TemplateFragment> stack) {
            return this.pointer;
        }
    }

    class FunctionTemplateFragment extends TemplateFragment {

        public FunctionTemplateFragment(String template, int start, int end, int expStart, int expEnd, int depth, int p) {
            super(template, start, end, expStart, expEnd, depth, p);
        }

        int eval(Bindings context, StringBuilder b, List<TemplateFragment> stack) {
            String function = template.substring(this.start, this.expressionStart);
            if (function.equals("lower")) {
                try {
                    Object value = engine.eval(expression, context);
                    b.append(String.valueOf(value).toLowerCase());
                } catch (ScriptException e) {
                    e.printStackTrace();
                }
            }
            return this.pointer;
        }
    }
}
